using NuGet.Common;
using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.Globalization;
using System.IO;
using System.Linq;

namespace NuGet.Commands
{
    [Command(typeof(NuGetCommand), "pack", "PackageCommandDescription", MaxArgs = 1, UsageSummaryResourceName = "PackageCommandUsageSummary",
            UsageDescriptionResourceName = "PackageCommandUsageDescription", UsageExampleResourceName = "PackCommandUsageExamples")]
    public class PackCommand : Command
    {
        internal static readonly string SymbolsExtension = ".symbols" + Constants.PackageExtension;

        private static readonly string[] _defaultExcludes = new[] {
            // Exclude previous package files
            @"**\*" + Constants.PackageExtension, 
            // Exclude all files and directories that begin with "."
            @"**\\.**", ".**"
        };

        // Target file paths to exclude when building the lib package for symbol server scenario
        private static readonly string[] _libPackageExcludes = new[] {
            @"**\*.pdb",
            @"src\**\*"
        };

        // Target file paths to exclude when building the symbols package for symbol server scenario
        private static readonly string[] _symbolPackageExcludes = new[] {
            @"content\**\*",
            @"tools\**\*.ps1"
        };

        private readonly HashSet<string> _excludes = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
        private readonly Dictionary<string, string> _properties = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);

        private static readonly HashSet<string> _allowedExtensions = new HashSet<string>(StringComparer.OrdinalIgnoreCase) {  
            Constants.ManifestExtension,
            ".csproj",
            ".vbproj",
            ".fsproj",
            ".nproj",
            ".btproj",
            ".dxjsproj"
        };

        private Version _minClientVersionValue;

        [Option(typeof(NuGetCommand), "PackageCommandOutputDirDescription")]
        public string OutputDirectory { get; set; }

        [Option(typeof(NuGetCommand), "PackageCommandBasePathDescription")]
        public string BasePath { get; set; }

        [Option(typeof(NuGetCommand), "PackageCommandVerboseDescription")]
        public bool Verbose { get; set; }

        [Option(typeof(NuGetCommand), "PackageCommandVersionDescription")]
        public string Version { get; set; }

        [Option(typeof(NuGetCommand), "PackageCommandExcludeDescription")]
        public ICollection<string> Exclude
        {
            get { return _excludes; }
        }

        [Option(typeof(NuGetCommand), "PackageCommandSymbolsDescription")]
        public bool Symbols { get; set; }

        [Option(typeof(NuGetCommand), "PackageCommandExcludeSourceCodeDescription")]
        public bool ExcludeSourceCode { get; set; }

        [Option(typeof(NuGetCommand), "PackageCommandToolDescription")]
        public bool Tool { get; set; }

        [Option(typeof(NuGetCommand), "PackageCommandBuildDescription")]
        public bool Build { get; set; }

        [Option(typeof(NuGetCommand), "PackageCommandNoDefaultExcludes")]
        public bool NoDefaultExcludes { get; set; }

        [Option(typeof(NuGetCommand), "PackageCommandNoRunAnalysis")]
        public bool NoPackageAnalysis { get; set; }

        [Option(typeof(NuGetCommand), "PackageCommandExcludeEmptyDirectories")]
        public bool ExcludeEmptyDirectories { get; set; }

        [Option(typeof(NuGetCommand), "PackageCommandIncludeReferencedProjects")]
        public bool IncludeReferencedProjects { get; set; }

        [Option(typeof(NuGetCommand), "PackageCommandPropertiesDescription")]
        public Dictionary<string, string> Properties
        {
            get
            {
                return _properties;
            }
        }

        [Option(typeof(NuGetCommand), "PackageCommandMinClientVersion")]
        public string MinClientVersion { get; set; }

        [Option(typeof(NuGetCommand), "PackageCommandBaseTargetPath")]
        public string BaseTargetPath { get; set; }

        [Option(typeof(NuGetCommand), "PackageCommandSolutionName")]
        public string SolutionName { get; set; }

        [Option(typeof(NuGetCommand), "PackageCommandDisableRules")]
        public string DisableRules { get; set; }


        [ImportMany]
        public IEnumerable<IPackageRule> Rules { get; set; }

        // TODO: Temporarily hide the real ConfigFile parameter from the help text.
        // When we fix #3230, we should remove this property.
        public new string ConfigFile { get; set; }

        public override void ExecuteCommand()
        {
            if (Verbose)
            {
                Console.WriteWarning(LocalizedResourceManager.GetString("Option_VerboseDeprecated"));
                Verbosity = Verbosity.Detailed;
            }

            // Get the input file
            string path = GetInputFile();

            Console.WriteLine(LocalizedResourceManager.GetString("PackageCommandAttemptingToBuildPackage"), Path.GetFileName(path));

            // If the BasePath is not specified, use the directory of the input file (nuspec / proj) file
            BasePath = String.IsNullOrEmpty(BasePath) ? Path.GetDirectoryName(Path.GetFullPath(path)) : BasePath;

            // Validate the BaseTargetPath, if it is provided
            if (!string.IsNullOrWhiteSpace(BaseTargetPath))
            {
                if (Build)
                {
                    // Providing BaseTargetPath only makes sense when the NuGet is not building the solution,
                    // i.e. the solution is built by TFS Team Build.
                    throw new CommandLineException(LocalizedResourceManager.GetString("PackageCommandIrrelevantBaseTargetPath"));
                }

                if (!Directory.Exists(BaseTargetPath))
                {
                    throw new CommandLineException(LocalizedResourceManager.GetString("PackageCommandInvalidBaseTargetPath"));
                }
            }

            if (!String.IsNullOrEmpty(MinClientVersion))
            {
                if (!System.Version.TryParse(MinClientVersion, out _minClientVersionValue))
                {
                    throw new CommandLineException(LocalizedResourceManager.GetString("PackageCommandInvalidMinClientVersion"));
                }
            }

            BuildPackage(path);
        }

        private void BuildPackage(PackageBuilder builder, bool analyzePackage, string outputPath = null)
        {
            if (!String.IsNullOrEmpty(Version))
            {
                builder.Version = new SemanticVersion(Version);
            }

            if (_minClientVersionValue != null)
            {
                builder.MinClientVersion = _minClientVersionValue;
            }

            outputPath = outputPath ?? GetOutputPath(builder);

            ExcludeFiles(builder.Files);
            // Track if the package file was already present on disk
            bool isExistingPackage = File.Exists(outputPath);
            try
            {
                using (Stream stream = File.Create(outputPath))
                {
                    builder.Save(stream);
                }
            }
            catch
            {
                if (!isExistingPackage && File.Exists(outputPath))
                {
                    File.Delete(outputPath);
                }
                throw;
            }

            if (Verbosity == Verbosity.Detailed)
            {
                PrintVerbose(outputPath);
            }

            var package = new OptimizedZipPackage(outputPath);
            if (analyzePackage)
            {
                AnalyzePackage(package);
            }

            Console.WriteLine(LocalizedResourceManager.GetString("PackageCommandSuccess"), outputPath);
        }

        private void PrintVerbose(string outputPath)
        {
            Console.WriteLine();
            var package = new OptimizedZipPackage(outputPath);

            Console.WriteLine("Id: {0}", package.Id);
            Console.WriteLine("Version: {0}", package.Version);
            Console.WriteLine("Authors: {0}", String.Join(", ", package.Authors));
            Console.WriteLine("Description: {0}", package.Description);
            if (package.LicenseUrl != null)
            {
                Console.WriteLine("License Url: {0}", package.LicenseUrl);
            }
            if (package.ProjectUrl != null)
            {
                Console.WriteLine("Project Url: {0}", package.ProjectUrl);
            }
            if (!String.IsNullOrEmpty(package.Tags))
            {
                Console.WriteLine("Tags: {0}", package.Tags.Trim());
            }
            if (package.DependencySets.Any())
            {
                Console.WriteLine("Dependencies: {0}", String.Join(", ", package.DependencySets.SelectMany(d => d.Dependencies).Select(d => d.ToString())));
            }
            else
            {
                Console.WriteLine("Dependencies: None");
            }

            Console.WriteLine();

            foreach (var file in package.GetFiles().OrderBy(p => p.Path))
            {
                Console.WriteLine(LocalizedResourceManager.GetString("PackageCommandAddedFile"), file.Path);
            }

            Console.WriteLine();
        }

        internal void ExcludeFiles(ICollection<IPackageFile> packageFiles)
        {
            // Always exclude the nuspec file
            // Review: This exclusion should be done by the package builder because it knows which file would collide with the auto-generated
            // manifest file.
            var wildCards = _excludes.Concat(new[] { @"**\*" + Constants.ManifestExtension });
            if (!NoDefaultExcludes)
            {
                // The user has not explicitly disabled default filtering.
                wildCards = wildCards.Concat(_defaultExcludes);
            }
            PathResolver.FilterPackageFiles(packageFiles, file => file.Path, wildCards);
        }

        private string GetOutputPath(PackageBuilder builder, bool symbols = false)
        {
            string version = String.IsNullOrEmpty(Version) ? builder.Version.ToString() : Version;

            // Output file is {id}.{version}
            string outputFile = builder.Id + "." + version;

            // If this is a source package then add .symbols.nupkg to the package file name
            if (symbols)
            {
                outputFile += SymbolsExtension;
            }
            else
            {
                outputFile += Constants.PackageExtension;
            }

            string outputDirectory = OutputDirectory ?? Directory.GetCurrentDirectory();
            return Path.Combine(outputDirectory, outputFile);
        }

        private void BuildPackage(string path)
        {
            string extension = Path.GetExtension(path);

            if (extension.Equals(Constants.ManifestExtension, StringComparison.OrdinalIgnoreCase))
            {
                BuildFromNuspec(path);
            }
            else
            {
                BuildFromProjectFile(path);
            }
        }

        private void BuildFromNuspec(string path)
        {
            PackageBuilder packageBuilder = CreatePackageBuilderFromNuspec(path);

            if (Symbols)
            {
                // remove source related files when building the lib package
                ExcludeFilesForLibPackage(packageBuilder.Files);

                if (!packageBuilder.Files.Any())
                {
                    throw new CommandLineException(String.Format(CultureInfo.CurrentCulture, LocalizedResourceManager.GetString("PackageCommandNoFilesForLibPackage"),
                        path, CommandLineConstants.NuGetDocs));
                }
            }

            BuildPackage(packageBuilder, !NoPackageAnalysis);

            if (Symbols)
            {
                BuildSymbolsPackage(path);
            }
        }

        private void BuildSymbolsPackage(string path)
        {
            PackageBuilder symbolsBuilder = CreatePackageBuilderFromNuspec(path);
            // remove unnecessary files when building the symbols package
            ExcludeFilesForSymbolPackage(symbolsBuilder.Files);

            if (!symbolsBuilder.Files.Any())
            {
                throw new CommandLineException(String.Format(CultureInfo.CurrentCulture, LocalizedResourceManager.GetString("PackageCommandNoFilesForSymbolsPackage"),
                        path, CommandLineConstants.NuGetDocs));
            }

            string outputPath = GetOutputPath(symbolsBuilder, symbols: true);
            BuildPackage(symbolsBuilder, false, outputPath);
        }

        internal static void ExcludeFilesForLibPackage(ICollection<IPackageFile> files)
        {
            PathResolver.FilterPackageFiles(files, file => file.Path, _libPackageExcludes);
        }

        internal static void ExcludeFilesForSymbolPackage(ICollection<IPackageFile> files)
        {
            PathResolver.FilterPackageFiles(files, file => file.Path, _symbolPackageExcludes);
        }

        private PackageBuilder CreatePackageBuilderFromNuspec(string path)
        {
            // Set the version property if the flag is set
            if (!String.IsNullOrEmpty(Version))
            {
                Properties["version"] = Version;
            }

            // Initialize the property provider based on what was passed in using the properties flag
            var propertyProvider = new DictionaryPropertyProvider(Properties);

            if (String.IsNullOrEmpty(BasePath))
            {
                return new PackageBuilder(path, propertyProvider, !ExcludeEmptyDirectories);
            }
            return new PackageBuilder(path, BasePath, propertyProvider, !ExcludeEmptyDirectories);
        }

        private void BuildFromProjectFile(string path)
        {
            var factory = new ProjectFactory(path, Properties)
            {
                IsTool = Tool,
                Logger = Console,
                Build = Build,
                IncludeReferencedProjects = IncludeReferencedProjects,
                BaseTargetPath = BaseTargetPath,
                SolutionName = SolutionName
            };

            // Add the additional Properties to the properties of the Project Factory
            foreach (var property in Properties)
            {
                if (factory.ProjectProperties.ContainsKey(property.Key))
                {
                    Console.WriteWarning(LocalizedResourceManager.GetString("Warning_DuplicatePropertyKey"), property.Key);
                }
                factory.ProjectProperties[property.Key] = property.Value;
            }

            // Create a builder for the main package as well as the sources/symbols package
            PackageBuilder mainPackageBuilder = factory.CreateBuilder(BasePath);

            // Build the main package
            BuildPackage(mainPackageBuilder, !NoPackageAnalysis);

            // If we're excluding symbols then do nothing else
            if (!Symbols)
            {
                return;
            }

            Console.WriteLine();
            Console.WriteLine(LocalizedResourceManager.GetString("PackageCommandAttemptingToBuildSymbolsPackage"), Path.GetFileName(path));

            factory.IncludeSymbols = true;
            factory.ExcludeSourceCode = ExcludeSourceCode;
            PackageBuilder symbolsBuilder = factory.CreateBuilder(BasePath);
            symbolsBuilder.Version = mainPackageBuilder.Version;

            // Get the file name for the sources package and build it
            string outputPath = GetOutputPath(symbolsBuilder, symbols: true);
            BuildPackage(symbolsBuilder, false, outputPath);

        }

        internal void AnalyzePackage(IPackage package)
        {
            var packageRules = new List<IPackageRule>();
            foreach (var rule in Rules)
            {
                // Convert the aggregate DefaultPackageRules into a set of individual rules to allow for filtering.
                // We cannot modify the DefaultPackageRules.Rules since it may be used by extensions, such as the "analyze" one.
                if (rule is DefaultPackageRules)
                {
                    packageRules.AddRange(DefaultPackageRules.RuleSet);
                }
                else
                {
                    packageRules.Add(rule);
                }
            }

            if (!String.IsNullOrEmpty(package.Version.SpecialVersion))
            {
                // If a package contains a special token, we'll warn users if it does not strictly follow semver guidelines.
                packageRules.Add(new StrictSemanticVersionValidationRule());
            }

            if (!String.IsNullOrWhiteSpace(DisableRules))
            {
                var disabledRulesList = DisableRules.Split(new [] {';'}, StringSplitOptions.RemoveEmptyEntries);
                packageRules.RemoveAll(rule => disabledRulesList.Any(disabled => string.Equals(rule.GetType().Name, disabled, StringComparison.OrdinalIgnoreCase)));
            }

            if (Verbosity == Verbosity.Detailed)
            {
                Console.WriteLine(LocalizedResourceManager.GetString("PackageCommandEnabledRules"), string.Join(";", packageRules.Select(rule => rule.GetType().Name)));
            }

            IList<PackageIssue> issues = package.Validate(packageRules).OrderBy(p => p.Title, StringComparer.CurrentCulture).ToList();

            if (issues.Count > 0)
            {
                Console.WriteLine();
                Console.WriteWarning(LocalizedResourceManager.GetString("PackageCommandPackageIssueSummary"), issues.Count, package.Id);
                foreach (var issue in issues)
                {
                    PrintPackageIssue(issue);
                }
            }
        }

        private void PrintPackageIssue(PackageIssue issue)
        {
            Console.WriteLine();
            Console.WriteWarning(
                prependWarningText: false,
                value: LocalizedResourceManager.GetString("PackageCommandIssueTitle"),
                args: issue.Title);

            Console.WriteWarning(
                prependWarningText: false,
                value: LocalizedResourceManager.GetString("PackageCommandIssueDescription"),
                args: issue.Description);

            if (!String.IsNullOrEmpty(issue.Solution))
            {
                Console.WriteWarning(
                    prependWarningText: false,
                    value: LocalizedResourceManager.GetString("PackageCommandIssueSolution"),
                    args: issue.Solution);
            }
        }

        private string GetInputFile()
        {
            IEnumerable<string> files = Arguments.Any() ? Arguments : Directory.GetFiles(Directory.GetCurrentDirectory());

            return GetInputFile(files);
        }

        internal static string GetInputFile(IEnumerable<string> files)
        {
            var candidates = files.Where(file => _allowedExtensions.Contains(Path.GetExtension(file)))
                                  .ToList();

            switch (candidates.Count)
            {
                case 1:
                    return candidates.Single();
                case 2:
                    // Remove all nuspec files
                    candidates.RemoveAll(file => Path.GetExtension(file).Equals(Constants.ManifestExtension, StringComparison.OrdinalIgnoreCase));
                    if (candidates.Count == 1)
                    {
                        return candidates.Single();
                    }
                    goto default;
                default:
                    throw new CommandLineException(LocalizedResourceManager.GetString("PackageCommandSpecifyInputFileError"));
            }
        }

        private class DictionaryPropertyProvider : IPropertyProvider
        {
            private readonly IDictionary<string, string> _properties;

            public DictionaryPropertyProvider(IDictionary<string, string> properties)
            {
                _properties = properties;
            }

            public dynamic GetPropertyValue(string propertyName)
            {
                string value;
                if (_properties.TryGetValue(propertyName, out value))
                {
                    return value;
                }
                return null;
            }
        }
    }
}